feat(agent-memory): Phase 8 — self-optimization config reads#256
feat(agent-memory): Phase 8 — self-optimization config reads#256jamby77 wants to merge 4 commits into
Conversation
eea7699 to
ee647cc
Compare
524f3a8 to
e58dbb7
Compare
14b6e7c to
9412e5a
Compare
e58dbb7 to
0d89d67
Compare
9412e5a to
37c7a4b
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 37c7a4b. Configure here.
0d89d67 to
73d91d7
Compare
37c7a4b to
06b56f6
Compare
KIvanow
left a comment
There was a problem hiding this comment.
Phase 8 is in good shape: off-by-default keeps existing stores quiet, the before-await snapshots are genuinely tear-free since applyConfig is synchronous and reassigns rather than mutates, the all-zero weights rejection is the right guard, and the config key stays clear of the index prefix. One thing I would like addressed:
A partial weights write silently resets the omitted components to constructor defaults. Because applyConfig rebuilds the weights vector from {...initialWeights} on every pass, a config hash that sets only recall.weights.similarity snaps recency and importance back to the constructor values (0.25 / 0.15) rather than preserving the currently-live ones. That is consistent with the documented snapshot-fallback semantics, but for Monitor's proposal engine, which is likely to nudge a single knob, it is a footgun: tuning one weight silently retunes the other two. Please either preserve the live weight components when only a subset is present, or document clearly that weights must always be written as a full triple so a partial proposal does not quietly reset the rest.
73d91d7 to
8bc0c13
Compare
7d0fa87 to
90ac526
Compare
8bc0c13 to
9f8485a
Compare
f325dee to
94e4101
Compare
94d2a7d to
66c0809
Compare
94e4101 to
ff2e1f1
Compare
- MemoryStore reads {name}:__mem_config on an opt-in configRefresh
interval and live-applies recall.threshold, recall.weights.*,
recall.halfLifeSeconds, and maxItemsPerScope without a restart
- Absent fields fall back to constructor values; invalid values are
ignored; reads are best-effort and never throw
- Reject an all-zero weight vector to keep recall ordering well-defined
- Add currentConfig() to expose the effective tunables; close() also
stops the refresh interval
- Refresh is off by default (opt-in) so a standalone store never polls
Capture halfLifeSeconds with threshold/weights before the first await so a concurrent configRefresh can't score a single recall with a mix of config versions.
enforceCapacity read weights and halfLifeSeconds from instance fields at selectEvictions time, several awaits after the capacity check. With opt-in configRefresh a refresh could land mid-pass and score victims with a different tunable set. Snapshot weights and halfLifeSeconds at entry, matching the recall snapshot, so a pass uses one consistent config.
…g write applyConfig rebuilt the weights vector from the constructor defaults each pass, so a config that set only one weight component silently reset the other two — a footgun for the proposal engine, which nudges a single knob. A partial write now starts from the live weights and overlays only what's present; a fully-absent weights config still falls back to the constructor values.
66c0809 to
c83f1e6
Compare
ff2e1f1 to
0efd8ba
Compare

Stacked on #255 (Phase 7 — discovery marker).
What
Phase 8 of
@betterdb/agent-memory: runtime-tunable recall/eviction knobs, so BetterDB Monitor's cache-proposals engine can retune a memory store without a restart (same pattern as semantic-cache'sconfigRefresh).MemoryStorereads{name}:__mem_configand live-applies:recall.threshold(cosine-distance ceiling, 0..2)recall.weights.similarity/recall.weights.recency/recall.weights.importancerecall.halfLifeSecondsmaxItemsPerScoperefreshConfig()(public, manual tick) reads the hash and applies it;currentConfig()exposes the effective tunables.Opt-in
configRefresh?: boolean | { enabled?, intervalMs? }enables an immediate read plus an unref'd interval (default 30s, min 1s);close()stops it.Design / review notes
.callsequences.initialWeightsis copied so the sharedDEFAULT_WEIGHTSconstant can't be aliased.recall()captures threshold+weights into locals before its firstawait, so an interval refresh can't tear an in-flight recall.Tests (
MemoryStore.config.test.ts, 12)defaults snapshot · threshold · weights · halfLifeSeconds+maxItemsPerScope · partial config leaves others default · field-removal reverts · invalid ignored · all-zero weights rejected · live threshold affects recall end-to-end · no polling when disabled · immediate+interval read & stop on close (fake timers) · best-effort on read failure.
77/77 package tests green ·
tscclean · prettier clean.Note
Medium Risk
Changes live recall scoring, threshold filtering, and eviction victim selection when refresh is enabled; defaults stay off and invalid config is ignored, but mis-tuned remote hashes could alter memory behavior at runtime.
Overview
Adds opt-in runtime tuning for
MemoryStoreby reading{name}:__mem_configfrom Valkey, mirroring semantic-cache’sconfigRefreshpattern so Monitor can retune without a restart.refreshConfig()andcurrentConfig()expose manual reads and the effective snapshot.configRefresh(off by default) triggers an immediateHGETALLplus an unref’d interval (default 30s, min 1s);close()clears the timer. Tunables includerecall.threshold, partial recall weight fields,recall.halfLifeSeconds, andmaxItemsPerScope, with validation, best-effort reads, rejection of all-zero weights, and constructor fallbacks when fields are missing.recall()andenforceCapacity()snapshot half-life (and eviction weights) so a concurrent refresh cannot mix config versions mid-operation. NewMemoryStore.config.test.tsand an eviction test cover the behavior;index.tsexports the new config types.Reviewed by Cursor Bugbot for commit 0efd8ba. Bugbot is set up for automated code reviews on this repo. Configure here.